package server

import (
	"bytes"
	"html/template"
	"strings"
)

var serviceTemplate = `
{{- /* delete empty line */ -}}
package service

import (
	{{- if .UseContext }}
	"context"
	{{- end }}
	{{- if .UseIO }}
	"io"
	{{- end }}

	"github.com/go-kratos/kratos/v2/log"
	{{- if .GoogleEmpty }}
	"google.golang.org/protobuf/types/known/emptypb"
	{{- end }}
	{{- if .GoogleWrapper }}
	"google.golang.org/protobuf/types/known/wrapperspb"
	{{- end }}

	pb "{{ .Package }}"
)
{{ range .Doc }}
// {{ . }}{{ end }}
type {{ .Service }}Service struct {
	pb.Unimplemented{{ .Service }}Server
	log *log.Helper
}

// New{{ .Service }}Server .
func New{{ .Service }}Server(logger *log.Helper) pb.{{ .Service }}Server {
	return &{{ .Service }}Service{
		log: logger,
	}
}{{ range .Methods }}
{{ range .Doc }}
// {{ . }}{{ end }}
{{- if eq .Type 1 }}
func (s *{{ .Service }}Service) {{ .Name }}(ctx context.Context, req *{{ .Request }}) (*{{ .Reply }}, error) {
	return &{{ .Reply }}{}, nil
}
{{- else if eq .Type 2 }}
func (s *{{ .Service }}Service) {{ .Name }}(conn pb.{{ .Service }}_{{ .Name }}Server) error {
	for {
		req, err := conn.Recv()
		if err == io.EOF {
			return nil
		}
		if err != nil {
			return err
		}
		
		err = conn.Send(&{{ .Reply }}{})
		if err != nil {
			return err
		}
	}
}
{{- else if eq .Type 3 }}
func (s *{{ .Service }}Service) {{ .Name }}(conn pb.{{ .Service }}_{{ .Name }}Server) error {
	for {
		req, err := conn.Recv()
		if err == io.EOF {
			return conn.SendAndClose(&{{ .Reply }}{})
		}
		if err != nil {
			return err
		}
	}
}
{{- else if eq .Type 4 }}
func (s *{{ .Service }}Service) {{ .Name }}(req {{ .Request }}, conn pb.{{ .Service }}_{{ .Name }}Server) error {
	for {
		err := conn.Send(&pb.{{ .Reply }}{})
		if err != nil {
			return err
		}
	}
}
{{- end }}
{{- end }}
`

type MethodType uint8

const (
	unaryType          MethodType = 1
	twoWayStreamsType  MethodType = 2
	requestStreamsType MethodType = 3
	returnsStreamsType MethodType = 4
)

// Service is a proto service.
type Service struct {
	TargetDir     string
	Package       string
	Service       string
	Methods       []*Method
	GoogleEmpty   bool
	GoogleWrapper bool

	UseIO      bool
	UseContext bool

	Doc []string
}

// Method is a proto method.
type Method struct {
	Service string
	Name    string
	Request string
	Reply   string
	Doc     []string

	// type: unary or stream
	Type MethodType
}

func (s *Service) execute() ([]byte, error) {
	buf := new(bytes.Buffer)
	for _, method := range s.Methods {
		request := method.Request
		reply := method.Reply
		if req, rep, ok := googleEmpty(method); ok {
			if req != "" {
				method.Request = req
			}
			if rep != "" {
				method.Reply = rep
			}
			s.GoogleEmpty = true
		}
		if req, rep, ok := googleWrapper(method); ok {
			if req != "" {
				method.Request = req
			}
			if rep != "" {
				method.Reply = rep
			}
			s.GoogleWrapper = true
		}
		if method.Type == twoWayStreamsType || method.Type == returnsStreamsType {
			s.UseIO = true
		}
		if method.Type == unaryType {
			s.UseContext = true
		}
		if !strings.HasPrefix(request, "google.protobuf.") {
			method.Request = "pb." + request
		}
		if !strings.HasPrefix(reply, "google.protobuf.") {
			method.Reply = "pb." + reply
		}
	}
	tmpl, err := template.New("service").Parse(serviceTemplate)
	if err != nil {
		return nil, err
	}
	if err := tmpl.Execute(buf, s); err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

func googleEmpty(method *Method) (req string, rep string, need bool) {
	if (method.Type == unaryType && method.Request == "google.protobuf.Empty") ||
		(method.Type == returnsStreamsType && method.Request == "google.protobuf.Empty") {
		req = "emptypb.Empty"
		need = true
	}
	if method.Type == unaryType && method.Reply == "google.protobuf.Empty" {
		rep = "emptypb.Empty"
		need = true
	}
	return
}

func googleWrapper(method *Method) (req string, rep string, need bool) {
	values := []string{
		"google.protobuf.DoubleValue",
		"google.protobuf.FloatValue",
		"google.protobuf.Int64Value",
		"google.protobuf.UInt64Value",
		"google.protobuf.Int32Value",
		"google.protobuf.UInt32Value",
		"google.protobuf.BoolValue",
		"google.protobuf.StringValue",
		"google.protobuf.BytesValue",
	}
	for _, v := range values {
		if (method.Type == unaryType && method.Request == v) ||
			(method.Type == returnsStreamsType && method.Request == v) {
			need = true
			req = "wrapperspb" + strings.Replace(v, "google.protobuf", "", 1)
		}
		if method.Type == unaryType && method.Reply == v {
			need = true
			rep = "wrapperspb" + strings.Replace(v, "google.protobuf", "", 1)
		}
	}
	return
}
